<?php
/**
 *  NOTE: Iterator methods iterate over all nodes in unknown order
 *  NOTE: it's impossible to create a tree element manually w/o tree
 *
 */
class Yz_Tree_Default
    extends Yz_Class
    implements ArrayAccess, Countable, Iterator, Yz_Tree_Interface
{

    /** a pseudo-root element i.e. the element is not in the tree
     *  but all the elements at depth 1 are its children
     *
     */
    protected $root = array();
    protected $rootId;

    /** All elements are added to __dict__
     *
     */

    /********************************
     *       Public methods         *
     ********************************/


    /** create a new tree or branch
     *  The $rootElementId is the parentId of a branch's top element(s) if it's not 0
     *  @param int|null $rootElementId
     *
     */
    public function __construct($rootElementId=null)
    {
        debug();

        if (null === $rootElementId) {

            $rootElementId = 0;
        } else {
            assert('is_scalar($rootElementId)');
            if ('0' !== (string)$rootElementId)
                $rootElementId = $this->adjustIndex($rootElementId);

        }
        // the elements with parent id = rootId are considered top-level
        $this->rootId = $rootElementId;
    }


    /** toString()
    *
    *   @return string
    */
    public function __toString()
    {
        $cl = $this->_class_();
        $count = $this->count();
        return "{$cl}[{$count}]";
    } // toString

    /********************************
     *      Iterator methods        *
     ********************************/



    /** create an undistributed (ie. no parent assigned) element in the tree
     *  @todo track the undistributed elements, to avoid operations on them ###
     *      the apply functions do that inherently
     *  @param int $id
     *  @param mixed $value
     *  @return Yz_Tree_Element_Interface
     */
    public function createElement($id, $value, $parentId=null)
    {
        debug();
        $tree =& $this->__dict__;
        $id = $this->adjustIndex($id);
        if (array_key_exists($id, $tree))
            throw IndexError::_new_(array('index' => $id));
        if ($parentId) {
            $parentId = $this->adjustIndex($parentId);
            // in case the tree's root nodes have a non-null parent-id,
            // and it is equal to the supplied one: add the node to root
            if ($parentId === $this->rootId)
                $parent = null;
            else if (isset($tree[$parentId]))
                $parent = $tree[$parentId];
            else
                throw IndexError::_new_(array('index' => $parentId));
        } else {
            $parent = null;
        }
        $el = new Yz_Tree_Element($this, $id, $value);
        $tree[$id] = $el;
        if ($parent)
            $parent->addChild($el);
        else
            $this->root[$id] = $el;
        return $el;
    }


    /**
     *  apply callbacks recursively to each element of the tree (i.e. all descendants of the element)
     *  NOTE: the processing order depends on implementation
     *      If the first callback returns non-empty, other callbacks are not executed
     *      If the 'enter' callback returns non-empty, the children iteration is skipped
     *  @param callback $callback: bool function(element)
     *  @param callback $enter: bool function(element)
     *  @param callback $exit: bool function(element)
     */
    public function apply($callback, $enter=null, $exit=null)
    {
        debug();

        assert('is_callable($callback)');
        assert('!$enter or is_callable($enter)');
        assert('!$exit or is_callable($exit)');

        foreach ($this->root as $ch)
            $this->_applyToElement($ch, $callback, $enter, $exit);
        return $this; //->_applyToElement(null, $callback, $enter, $exit);
    }

    /**
    *   @return bool
    */
    public function inTree( Yz_Tree_Element_Interface $el )
    {
        debug();
        $id = $el->id();
        return isset($this->__dict__[$id])
            and $this->__dict__[$id] === $el;
    }


    /*******************************
    ****   Protected methods     ***
    *******************************/

    protected function adjustIndex($index)
    {
        if (! is_numeric($index))
            throw TypeError::_new_();
        if ($index <= 0)
            throw ValueError::_new_();
        return $index;
    }

    /** the default setter is called after index verification
     *  @param string index
     *  @return chainable
     *  @todo add value as a child
     */
    protected function _set_( $index, $value )
    {
        throw AttributeError::_new_(array('attr' => $index));
    }

    /** the default unsetter is called after index verification
     *  @param string index
     *  @return chainable
     *  @todo remove element/branch
     */
    protected function _del_( $index )
    {
        throw AttributeError::_new_(array('attr' => $index));
    }


    /**
     *  recursively apply callbacks to element and children
     *  If $callback returns non-empty, do not execute other callbacks nor iterate over children
     *  If $enter returns non-empty, do not iterate over children but do execute $exit
     */
    protected function _applyToElement($el, $callback, $enter, $exit)
    {
        $r = call_user_func( $callback, $el );

        if (! $r) {

            $r = ($enter and call_user_func( $enter, $el ));
            if (! $r)
                foreach ($el as $ch)
                    $this->_applyToElement($ch, $callback, $enter, $exit);
            $exit and call_user_func( $exit, $el );
        }
        return $this;
    }


}
